{
"cells": [
{
"cell_type": "markdown",
"id": "7a2d87c4",
"metadata": {},
"source": [
"# Multiple Time Intervals\n",
"\n",
"You may want to make trading decisions on a different timeframe than your underlying data source. For instance, you might choose to execute trades on daily bars after confirming the trend on weekly or monthly bars. **PyBroker v2** supports compressing backtest data into longer intervals and making those compressed bars available to your strategy."
]
},
{
"cell_type": "markdown",
"id": "a7400671",
"metadata": {},
"source": [
"## Interval Types\n",
"\n",
"You can define an interval using any of these three formats:\n",
"\n",
"* **Every-n-bars** (`int` greater than `1`): Compresses every `n` base bars into one bar. Using `5` on daily data produces one bar per five trading days.\n",
"* **Duration** (`str`): A fixed time span written as digits followed by a single unit letter (`s`, `m`, `h`, or `d`). Passing `\"5m\"` compresses 1-minute bars into 5-minute bars.\n",
"* **Calendar** (`str`): Aligns compressed bars to calendar boundaries using one of the following options:\n",
"\n",
"| Calendar String | Boundary Alignment |\n",
"| :--- | :--- |\n",
"| `\"daily\"` | Standard daily boundary. |\n",
"| `\"weekly\"` | Starts on Monday. |\n",
"| `\"monthly\"` | Starts on the 1st of the month. |\n",
"| `\"quarterly\"` | Begins in January, April, July, and October. |\n",
"| `\"yearly\"` | Starts on January 1. |\n",
"\n",
"Your chosen interval must always be longer than the bars being compressed. For example, if you fetch daily bars from [YFinance](https://www.pybroker.com/en/latest/reference/pybroker.data.html#pybroker.data.YFinance), then `\"weekly\"` and `\"monthly\"` are valid intervals. Attempting to use `\"daily\"` or `\"1h\"` will raise a `ValueError`.\n",
"\n",
"Before using intervals in a strategy, let's build some intuition by compressing bars directly. We will start by downloading daily data:"
]
},
{
"cell_type": "code",
"execution_count": 1,
"id": "065e06fd",
"metadata": {
"execution": {
"iopub.execute_input": "2026-08-11T20:31:08.852538Z",
"iopub.status.busy": "2026-08-11T20:31:08.852436Z",
"iopub.status.idle": "2026-08-11T20:31:09.995798Z",
"shell.execute_reply": "2026-08-11T20:31:09.995169Z"
}
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Loading bar data...\n"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
"\r",
"[ 0% ]"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
"\r",
"[**********************67%******* ] 2 of 3 completed"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
"\r",
"[*********************100%***********************] 3 of 3 completed"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
"\n"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"Loaded bar data: 0:00:00 \n",
"\n"
]
},
{
"data": {
"text/html": [
"
\n",
"\n",
"
\n",
" \n",
" \n",
" | \n",
" date | \n",
" symbol | \n",
" open | \n",
" high | \n",
" low | \n",
" close | \n",
" volume | \n",
" adj_close | \n",
"
\n",
" \n",
" \n",
" \n",
" | 0 | \n",
" 2021-01-04 | \n",
" AMD | \n",
" 92.110001 | \n",
" 96.059998 | \n",
" 90.919998 | \n",
" 92.300003 | \n",
" 51802600 | \n",
" 92.300003 | \n",
"
\n",
" \n",
" | 1 | \n",
" 2021-01-04 | \n",
" INTC | \n",
" 49.889999 | \n",
" 51.389999 | \n",
" 49.400002 | \n",
" 49.669998 | \n",
" 46102500 | \n",
" 44.902931 | \n",
"
\n",
" \n",
" | 2 | \n",
" 2021-01-04 | \n",
" NVDA | \n",
" 13.104250 | \n",
" 13.652500 | \n",
" 12.962500 | \n",
" 13.113500 | \n",
" 560640000 | \n",
" 13.060796 | \n",
"
\n",
" \n",
" | 3 | \n",
" 2021-01-05 | \n",
" AMD | \n",
" 92.099998 | \n",
" 93.209999 | \n",
" 91.410004 | \n",
" 92.769997 | \n",
" 34208000 | \n",
" 92.769997 | \n",
"
\n",
" \n",
" | 4 | \n",
" 2021-01-05 | \n",
" INTC | \n",
" 49.450001 | \n",
" 50.830002 | \n",
" 49.330002 | \n",
" 50.610001 | \n",
" 24866600 | \n",
" 45.752716 | \n",
"
\n",
" \n",
"
\n",
"
"
],
"text/plain": [
" date symbol open high low close volume \\\n",
"0 2021-01-04 AMD 92.110001 96.059998 90.919998 92.300003 51802600 \n",
"1 2021-01-04 INTC 49.889999 51.389999 49.400002 49.669998 46102500 \n",
"2 2021-01-04 NVDA 13.104250 13.652500 12.962500 13.113500 560640000 \n",
"3 2021-01-05 AMD 92.099998 93.209999 91.410004 92.769997 34208000 \n",
"4 2021-01-05 INTC 49.450001 50.830002 49.330002 50.610001 24866600 \n",
"\n",
" adj_close \n",
"0 92.300003 \n",
"1 44.902931 \n",
"2 13.060796 \n",
"3 92.769997 \n",
"4 45.752716 "
]
},
"execution_count": 1,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"import pybroker\n",
"from pybroker import Strategy, YFinance\n",
"\n",
"pybroker.enable_data_source_cache(\"multiple_time_intervals\")\n",
"\n",
"yfinance = YFinance()\n",
"df = yfinance.query(\n",
" [\"AMD\", \"NVDA\", \"INTC\"], start_date=\"1/1/2021\", end_date=\"1/1/2026\"\n",
")\n",
"df.head()"
]
},
{
"cell_type": "markdown",
"id": "1b5ccc4a",
"metadata": {},
"source": [
"## Compressing Bars\n",
"\n",
"The [compress_bars](https://www.pybroker.com/en/latest/reference/pybroker.interval.html#pybroker.interval.compress_bars) function converts OHLCV data (either a [Pandas DataFrame](https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.html) or [BarData](https://www.pybroker.com/en/latest/reference/pybroker.common.html#pybroker.common.BarData)) to a longer interval, returning the result as a new [BarData](https://www.pybroker.com/en/latest/reference/pybroker.common.html#pybroker.common.BarData) object. Every compressed bar is timestamped with the date of the last base bar it contains.\n",
"\n",
"When grouping base bars into a compressed bar, the data is aggregated as follows:\n",
"* **Open:** Taken from the first base bar.\n",
"* **High / Low:** The highest high and lowest low.\n",
"* **Close:** Taken from the last base bar.\n",
"* **Volume:** The sum of the volumes.\n",
"* **VWAP:** The volume-weighted average.\n",
"* **Custom columns:** The last value in the period (e.g., [YFinance](https://www.pybroker.com/en/latest/reference/pybroker.data.html#pybroker.data.YFinance)'s `adj_close`).\n",
"\n",
"You must also supply the `base_timeframe` parameter to declare the spacing of your input bars (for example, `\"1d\"` for daily data). \n",
"\n",
"Let's compress AMD into calendar weeks and view the result as a [Pandas DataFrame](https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.html) with [bars_to_df](https://www.pybroker.com/en/latest/reference/pybroker.common.html#pybroker.common.bars_to_df):"
]
},
{
"cell_type": "code",
"execution_count": 2,
"id": "9a45f848",
"metadata": {
"execution": {
"iopub.execute_input": "2026-08-11T20:31:09.997251Z",
"iopub.status.busy": "2026-08-11T20:31:09.997051Z",
"iopub.status.idle": "2026-08-11T20:31:10.615768Z",
"shell.execute_reply": "2026-08-11T20:31:10.615228Z"
}
},
"outputs": [
{
"data": {
"text/html": [
"\n",
"\n",
"
\n",
" \n",
" \n",
" | \n",
" date | \n",
" open | \n",
" high | \n",
" low | \n",
" close | \n",
" volume | \n",
" adj_close | \n",
"
\n",
" \n",
" \n",
" \n",
" | 0 | \n",
" 2021-01-08 | \n",
" 92.110001 | \n",
" 96.400002 | \n",
" 89.459999 | \n",
" 94.580002 | \n",
" 220635900.0 | \n",
" 94.580002 | \n",
"
\n",
" \n",
" | 1 | \n",
" 2021-01-15 | \n",
" 94.029999 | \n",
" 99.230003 | \n",
" 87.860001 | \n",
" 88.209999 | \n",
" 279733900.0 | \n",
" 88.209999 | \n",
"
\n",
" \n",
" | 2 | \n",
" 2021-01-22 | \n",
" 89.559998 | \n",
" 95.949997 | \n",
" 87.239998 | \n",
" 92.790001 | \n",
" 205817500.0 | \n",
" 92.790001 | \n",
"
\n",
" \n",
" | 3 | \n",
" 2021-01-29 | \n",
" 94.139999 | \n",
" 95.739998 | \n",
" 85.019997 | \n",
" 85.639999 | \n",
" 291661400.0 | \n",
" 85.639999 | \n",
"
\n",
" \n",
" | 4 | \n",
" 2021-02-05 | \n",
" 86.830002 | \n",
" 89.480003 | \n",
" 84.660004 | \n",
" 87.900002 | \n",
" 169582500.0 | \n",
" 87.900002 | \n",
"
\n",
" \n",
"
\n",
"
"
],
"text/plain": [
" date open high low close volume \\\n",
"0 2021-01-08 92.110001 96.400002 89.459999 94.580002 220635900.0 \n",
"1 2021-01-15 94.029999 99.230003 87.860001 88.209999 279733900.0 \n",
"2 2021-01-22 89.559998 95.949997 87.239998 92.790001 205817500.0 \n",
"3 2021-01-29 94.139999 95.739998 85.019997 85.639999 291661400.0 \n",
"4 2021-02-05 86.830002 89.480003 84.660004 87.900002 169582500.0 \n",
"\n",
" adj_close \n",
"0 94.580002 \n",
"1 88.209999 \n",
"2 92.790001 \n",
"3 85.639999 \n",
"4 87.900002 "
]
},
"execution_count": 2,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"from pybroker import compress_bars\n",
"from pybroker.common import bars_to_df\n",
"\n",
"\n",
"amd_df = df[df[\"symbol\"] == \"AMD\"]\n",
"bars_to_df(compress_bars(amd_df, \"weekly\", base_timeframe=\"1d\")).head()"
]
},
{
"cell_type": "markdown",
"id": "1a2ca562",
"metadata": {},
"source": [
"Every-n-bars compression works the same way. In this example, every `5` daily bars become one bar:"
]
},
{
"cell_type": "code",
"execution_count": 3,
"id": "03d4b55d",
"metadata": {
"execution": {
"iopub.execute_input": "2026-08-11T20:31:10.616926Z",
"iopub.status.busy": "2026-08-11T20:31:10.616752Z",
"iopub.status.idle": "2026-08-11T20:31:10.623035Z",
"shell.execute_reply": "2026-08-11T20:31:10.622594Z"
}
},
"outputs": [
{
"data": {
"text/html": [
"\n",
"\n",
"
\n",
" \n",
" \n",
" | \n",
" date | \n",
" open | \n",
" high | \n",
" low | \n",
" close | \n",
" volume | \n",
" adj_close | \n",
"
\n",
" \n",
" \n",
" \n",
" | 0 | \n",
" 2021-01-08 | \n",
" 92.110001 | \n",
" 96.400002 | \n",
" 89.459999 | \n",
" 94.580002 | \n",
" 220635900.0 | \n",
" 94.580002 | \n",
"
\n",
" \n",
" | 1 | \n",
" 2021-01-15 | \n",
" 94.029999 | \n",
" 99.230003 | \n",
" 87.860001 | \n",
" 88.209999 | \n",
" 279733900.0 | \n",
" 88.209999 | \n",
"
\n",
" \n",
" | 2 | \n",
" 2021-01-25 | \n",
" 89.559998 | \n",
" 95.949997 | \n",
" 87.239998 | \n",
" 94.129997 | \n",
" 260904400.0 | \n",
" 94.129997 | \n",
"
\n",
" \n",
" | 3 | \n",
" 2021-02-01 | \n",
" 94.910004 | \n",
" 95.720001 | \n",
" 84.660004 | \n",
" 87.660004 | \n",
" 278933800.0 | \n",
" 87.660004 | \n",
"
\n",
" \n",
" | 4 | \n",
" 2021-02-08 | \n",
" 88.489998 | \n",
" 91.989998 | \n",
" 86.879997 | \n",
" 91.470001 | \n",
" 174863100.0 | \n",
" 91.470001 | \n",
"
\n",
" \n",
"
\n",
"
"
],
"text/plain": [
" date open high low close volume \\\n",
"0 2021-01-08 92.110001 96.400002 89.459999 94.580002 220635900.0 \n",
"1 2021-01-15 94.029999 99.230003 87.860001 88.209999 279733900.0 \n",
"2 2021-01-25 89.559998 95.949997 87.239998 94.129997 260904400.0 \n",
"3 2021-02-01 94.910004 95.720001 84.660004 87.660004 278933800.0 \n",
"4 2021-02-08 88.489998 91.989998 86.879997 91.470001 174863100.0 \n",
"\n",
" adj_close \n",
"0 94.580002 \n",
"1 88.209999 \n",
"2 94.129997 \n",
"3 87.660004 \n",
"4 91.470001 "
]
},
"execution_count": 3,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"bars_to_df(compress_bars(amd_df, 5, base_timeframe=\"1d\")).head()"
]
},
{
"cell_type": "markdown",
"id": "af6be7a5",
"metadata": {},
"source": [
"## A Multi-Timeframe Strategy\n",
"\n",
"To use higher timeframes in your backtest, pass the `intervals` parameter to [add_execution](https://www.pybroker.com/en/latest/reference/pybroker.strategy.html#pybroker.strategy.Strategy.add_execution). Your execution function can then access the compressed bars through [ctx.interval](https://www.pybroker.com/en/latest/reference/pybroker.context.html#pybroker.context.ExecContext.interval), which returns a read-only [IntervalContext](https://www.pybroker.com/en/latest/reference/pybroker.context.html#pybroker.context.IntervalContext). \n",
"\n",
"Using the `intervals` parameter provides compressed bars only. Indicators and models are never computed on these intervals unless you bind them explicitly, as shown later in this notebook.\n",
"\n",
"To prevent look-ahead bias, [ctx.interval](https://www.pybroker.com/en/latest/reference/pybroker.context.html#pybroker.context.ExecContext.interval) only ever exposes *completed* bars. For example, the week or month that is currently forming is never visible, ensuring that future data cannot leak into your daily trading decisions.\n",
"\n",
"In the following strategy, we will execute trades on daily bars while using longer intervals to generate different trading signals:\n",
"\n",
"* **Monthly (Regime):** Only enter when the last completed monthly close is higher than the close from three months ago.\n",
"* **Weekly (Trend):** Only enter when the last completed weekly close is higher than the close from ten weeks ago, and exit when it falls below.\n",
"* **Daily (Timing):** Enter on the first daily close above the last completed weekly close.\n"
]
},
{
"cell_type": "code",
"execution_count": 4,
"id": "41ef50cc",
"metadata": {
"execution": {
"iopub.execute_input": "2026-08-11T20:31:10.623986Z",
"iopub.status.busy": "2026-08-11T20:31:10.623900Z",
"iopub.status.idle": "2026-08-11T20:31:10.741221Z",
"shell.execute_reply": "2026-08-11T20:31:10.740766Z"
}
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Backtesting: 2021-01-01 00:00:00 to 2026-01-01 00:00:00\n",
"\n"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"Loaded cached bar data.\n",
"\n"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"Test split: 2021-01-04 00:00:00 to 2025-12-31 00:00:00\n"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
"\r",
"\u001b[38;2;255;0;0m 0%\u001b[39m \u001b[38;2;255;0;0m(0 of 1255)\u001b[39m | | Elapsed Time: 0:00:00 ETA: --:--:--"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
"\r",
"\u001b[38;2;187;255;0m 81%\u001b[39m \u001b[38;2;187;255;0m(1021 of 1255)\u001b[39m |################ | Elapsed Time: 0:00:00 ETA: 0:00:00"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
"\r",
"\u001b[38;2;0;255;0m100%\u001b[39m \u001b[38;2;0;255;0m(1255 of 1255)\u001b[39m |####################| Elapsed Time: 0:00:00 Time: 0:00:00"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
"\n"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"\n"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"Finished backtest: 0:00:00\n"
]
},
{
"data": {
"text/html": [
"\n",
"\n",
"
\n",
" \n",
" \n",
" | \n",
" name | \n",
" value | \n",
"
\n",
" \n",
" \n",
" \n",
" | 0 | \n",
" trade_count | \n",
" 25 | \n",
"
\n",
" \n",
" | 1 | \n",
" initial_market_value | \n",
" 100000.0 | \n",
"
\n",
" \n",
" | 2 | \n",
" end_market_value | \n",
" 118433.0 | \n",
"
\n",
" \n",
" | 3 | \n",
" total_pnl | \n",
" 18433.0 | \n",
"
\n",
" \n",
" | 4 | \n",
" unrealized_pnl | \n",
" 0.0 | \n",
"
\n",
" \n",
" | 5 | \n",
" total_return_pct | \n",
" 18.433 | \n",
"
\n",
" \n",
" | 6 | \n",
" total_profit | \n",
" 28083.0 | \n",
"
\n",
" \n",
" | 7 | \n",
" total_loss | \n",
" -9650.0 | \n",
"
\n",
" \n",
" | 8 | \n",
" total_fees | \n",
" 0.0 | \n",
"
\n",
" \n",
" | 9 | \n",
" max_drawdown | \n",
" -9555.0 | \n",
"
\n",
" \n",
" | 10 | \n",
" max_drawdown_pct | \n",
" -7.797903 | \n",
"
\n",
" \n",
" | 11 | \n",
" max_drawdown_date | \n",
" 2025-05-19 00:00:00 | \n",
"
\n",
" \n",
" | 12 | \n",
" win_rate | \n",
" 60.0 | \n",
"
\n",
" \n",
" | 13 | \n",
" loss_rate | \n",
" 40.0 | \n",
"
\n",
" \n",
" | 14 | \n",
" winning_trades | \n",
" 15 | \n",
"
\n",
" \n",
" | 15 | \n",
" losing_trades | \n",
" 10 | \n",
"
\n",
" \n",
" | 16 | \n",
" avg_pnl | \n",
" 737.32 | \n",
"
\n",
" \n",
" | 17 | \n",
" avg_return_pct | \n",
" 18.0996 | \n",
"
\n",
" \n",
" | 18 | \n",
" avg_trade_bars | \n",
" 57.08 | \n",
"
\n",
" \n",
" | 19 | \n",
" avg_profit | \n",
" 1872.2 | \n",
"
\n",
" \n",
"
\n",
"
"
],
"text/plain": [
" name value\n",
"0 trade_count 25\n",
"1 initial_market_value 100000.0\n",
"2 end_market_value 118433.0\n",
"3 total_pnl 18433.0\n",
"4 unrealized_pnl 0.0\n",
"5 total_return_pct 18.433\n",
"6 total_profit 28083.0\n",
"7 total_loss -9650.0\n",
"8 total_fees 0.0\n",
"9 max_drawdown -9555.0\n",
"10 max_drawdown_pct -7.797903\n",
"11 max_drawdown_date 2025-05-19 00:00:00\n",
"12 win_rate 60.0\n",
"13 loss_rate 40.0\n",
"14 winning_trades 15\n",
"15 losing_trades 10\n",
"16 avg_pnl 737.32\n",
"17 avg_return_pct 18.0996\n",
"18 avg_trade_bars 57.08\n",
"19 avg_profit 1872.2"
]
},
"execution_count": 4,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"def buy_with_trend(ctx):\n",
" weekly = ctx.interval(\"weekly\")\n",
" monthly = ctx.interval(\"monthly\")\n",
" # Wait until enough completed weekly and monthly bars exist.\n",
" if len(weekly.close) < 10 or len(monthly.close) < 4:\n",
" return\n",
" regime_up = monthly.close[-1] > monthly.close[-4]\n",
" trend_up = weekly.close[-1] > weekly.close[-10]\n",
" pos = ctx.long_pos()\n",
" if not pos and regime_up and trend_up and ctx.close[-1] > weekly.close[-1]:\n",
" ctx.buy_shares = 100\n",
" elif pos and not trend_up:\n",
" ctx.sell_all_shares()\n",
"\n",
"\n",
"strategy = Strategy(yfinance, start_date=\"1/1/2021\", end_date=\"1/1/2026\")\n",
"strategy.add_execution(\n",
" buy_with_trend,\n",
" [\"AMD\", \"NVDA\", \"INTC\"],\n",
" intervals=[\"weekly\", \"monthly\"],\n",
")\n",
"result = strategy.backtest(timeframe=\"1d\")\n",
"result.metrics_df.head(20)"
]
},
{
"cell_type": "markdown",
"id": "80263d36",
"metadata": {},
"source": [
"## Binding an Indicator to an Interval\n",
"\n",
"To compute an indicator on compressed bars, bind it to one or more intervals with [Indicator.intervals(...)](https://www.pybroker.com/en/latest/reference/pybroker.indicator.html#pybroker.indicator.Indicator.intervals).\n",
"\n",
"The example below updates the weekly trend rule to compare the weekly close against a 10-bar SMA calculated from the weekly bars:"
]
},
{
"cell_type": "code",
"execution_count": 5,
"id": "9774d6ad",
"metadata": {
"execution": {
"iopub.execute_input": "2026-08-11T20:31:10.742847Z",
"iopub.status.busy": "2026-08-11T20:31:10.742749Z",
"iopub.status.idle": "2026-08-11T20:31:10.746841Z",
"shell.execute_reply": "2026-08-11T20:31:10.746301Z"
}
},
"outputs": [],
"source": [
"from pybroker.vect import sumv\n",
"\n",
"sma_10 = pybroker.indicator(\"sma_10\", lambda data: sumv(data.close, 10) / 10)\n",
"\n",
"\n",
"def buy_with_indicator(ctx):\n",
" weekly = ctx.interval(\"weekly\")\n",
" monthly = ctx.interval(\"monthly\")\n",
" # Wait until enough completed weekly and monthly bars exist.\n",
" if len(weekly.close) < 10 or len(monthly.close) < 4:\n",
" return\n",
" wk_sma = weekly.indicator(\"sma_10\")\n",
" regime_up = monthly.close[-1] > monthly.close[-4]\n",
" trend_up = weekly.close[-1] > wk_sma[-1]\n",
" pos = ctx.long_pos()\n",
" if not pos and regime_up and trend_up and ctx.close[-1] > wk_sma[-1]:\n",
" ctx.buy_shares = 100\n",
" elif pos and not trend_up:\n",
" ctx.sell_all_shares()\n",
"\n",
"\n",
"strategy = Strategy(yfinance, start_date=\"1/1/2021\", end_date=\"1/1/2026\")\n",
"strategy.add_execution(\n",
" buy_with_indicator,\n",
" [\"AMD\", \"NVDA\", \"INTC\"],\n",
" indicators=sma_10.intervals(\"weekly\"),\n",
" intervals=\"monthly\",\n",
")"
]
},
{
"cell_type": "markdown",
"id": "641cf137-8c3f-4e33-acf2-1c5c5c3e9ac4",
"metadata": {},
"source": [
"**PyBroker** automatically combines bound intervals with those in the execution's `intervals` parameter. Here, the `\"weekly\"` interval is made accessible via [ctx.interval(\"weekly\")](https://www.pybroker.com/en/latest/reference/pybroker.context.html#pybroker.context.ExecContext.interval) and the `intervals` parameter only needs to specify `\"monthly\"` for the raw monthly bars.\n",
"\n",
"Note that the binding will override also computing the indicator on the base timeframe of the data source. To also compute the indicator on the base timeframe of the data source, pass `\"base\"` to [Indicator.intervals()](https://www.pybroker.com/en/latest/reference/pybroker.indicator.html#pybroker.indicator.Indicator.intervals)."
]
},
{
"cell_type": "code",
"execution_count": 6,
"id": "286021ba-0623-497d-91b2-3ebf7282ca8f",
"metadata": {
"execution": {
"iopub.execute_input": "2026-08-11T20:31:10.748198Z",
"iopub.status.busy": "2026-08-11T20:31:10.748101Z",
"iopub.status.idle": "2026-08-11T20:31:10.851972Z",
"shell.execute_reply": "2026-08-11T20:31:10.851489Z"
}
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Backtesting: 2021-01-01 00:00:00 to 2026-01-01 00:00:00\n",
"\n"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"Loaded cached bar data.\n",
"\n"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"Computing indicators...\n"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
"\r",
"\u001b[38;2;255;0;0m 0%\u001b[39m \u001b[38;2;255;0;0m(0 of 3)\u001b[39m | | Elapsed Time: 0:00:00 ETA: --:--:--"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
"\r",
"\u001b[38;2;0;255;0m100%\u001b[39m \u001b[38;2;0;255;0m(3 of 3)\u001b[39m |##########################| Elapsed Time: 0:00:00 Time: 0:00:00"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
"\n"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"\n"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"Test split: 2021-01-04 00:00:00 to 2025-12-31 00:00:00\n"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
"\r",
"\u001b[38;2;255;0;0m 0%\u001b[39m \u001b[38;2;255;0;0m(0 of 1255)\u001b[39m | | Elapsed Time: 0:00:00 ETA: --:--:--"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
"\r",
"\u001b[38;2;200;255;0m 76%\u001b[39m \u001b[38;2;200;255;0m(961 of 1255)\u001b[39m |################ | Elapsed Time: 0:00:00 ETA: 0:00:00"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
"\r",
"\u001b[38;2;0;255;0m100%\u001b[39m \u001b[38;2;0;255;0m(1255 of 1255)\u001b[39m |####################| Elapsed Time: 0:00:00 Time: 0:00:00"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
"\n"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"\n"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"Finished backtest: 0:00:00\n"
]
},
{
"data": {
"text/html": [
"\n",
"\n",
"
\n",
" \n",
" \n",
" | \n",
" name | \n",
" value | \n",
"
\n",
" \n",
" \n",
" \n",
" | 0 | \n",
" trade_count | \n",
" 36 | \n",
"
\n",
" \n",
" | 1 | \n",
" initial_market_value | \n",
" 100000.0 | \n",
"
\n",
" \n",
" | 2 | \n",
" end_market_value | \n",
" 120278.0 | \n",
"
\n",
" \n",
" | 3 | \n",
" total_pnl | \n",
" 20361.0 | \n",
"
\n",
" \n",
" | 4 | \n",
" unrealized_pnl | \n",
" -83.0 | \n",
"
\n",
" \n",
" | 5 | \n",
" total_return_pct | \n",
" 20.361 | \n",
"
\n",
" \n",
" | 6 | \n",
" total_profit | \n",
" 31960.0 | \n",
"
\n",
" \n",
" | 7 | \n",
" total_loss | \n",
" -11599.0 | \n",
"
\n",
" \n",
" | 8 | \n",
" total_fees | \n",
" 0.0 | \n",
"
\n",
" \n",
" | 9 | \n",
" max_drawdown | \n",
" -9555.0 | \n",
"
\n",
" \n",
" | 10 | \n",
" max_drawdown_pct | \n",
" -7.398317 | \n",
"
\n",
" \n",
" | 11 | \n",
" max_drawdown_date | \n",
" 2025-11-21 00:00:00 | \n",
"
\n",
" \n",
" | 12 | \n",
" win_rate | \n",
" 47.222222 | \n",
"
\n",
" \n",
" | 13 | \n",
" loss_rate | \n",
" 52.777778 | \n",
"
\n",
" \n",
" | 14 | \n",
" winning_trades | \n",
" 17 | \n",
"
\n",
" \n",
" | 15 | \n",
" losing_trades | \n",
" 19 | \n",
"
\n",
" \n",
" | 16 | \n",
" avg_pnl | \n",
" 565.583333 | \n",
"
\n",
" \n",
" | 17 | \n",
" avg_return_pct | \n",
" 10.874444 | \n",
"
\n",
" \n",
" | 18 | \n",
" avg_trade_bars | \n",
" 38.972222 | \n",
"
\n",
" \n",
" | 19 | \n",
" avg_profit | \n",
" 1880.0 | \n",
"
\n",
" \n",
"
\n",
"
"
],
"text/plain": [
" name value\n",
"0 trade_count 36\n",
"1 initial_market_value 100000.0\n",
"2 end_market_value 120278.0\n",
"3 total_pnl 20361.0\n",
"4 unrealized_pnl -83.0\n",
"5 total_return_pct 20.361\n",
"6 total_profit 31960.0\n",
"7 total_loss -11599.0\n",
"8 total_fees 0.0\n",
"9 max_drawdown -9555.0\n",
"10 max_drawdown_pct -7.398317\n",
"11 max_drawdown_date 2025-11-21 00:00:00\n",
"12 win_rate 47.222222\n",
"13 loss_rate 52.777778\n",
"14 winning_trades 17\n",
"15 losing_trades 19\n",
"16 avg_pnl 565.583333\n",
"17 avg_return_pct 10.874444\n",
"18 avg_trade_bars 38.972222\n",
"19 avg_profit 1880.0"
]
},
"execution_count": 6,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"result = strategy.backtest(timeframe=\"1d\")\n",
"result.metrics_df.head(20)"
]
},
{
"cell_type": "markdown",
"id": "7ac2ec55",
"metadata": {},
"source": [
"## Training a Model on an Interval\n",
"\n",
"You can bind models in the same way with [ModelSource.intervals(...)](https://www.pybroker.com/en/latest/reference/pybroker.model.html#pybroker.model.ModelSource.intervals). **PyBroker** will then train models for each interval using the interval's compressed bars and any registered indicators. You can then access the per-interval predictions by calling the [preds](https://www.pybroker.com/en/latest/reference/pybroker.context.html#pybroker.context.IntervalContext.preds) method on that interval's context.\n",
"\n",
"This example trains a [LinearRegression](https://scikit-learn.org/stable/modules/generated/sklearn.linear_model.LinearRegression.html) model to predict the next weekly return from the weekly close:"
]
},
{
"cell_type": "code",
"execution_count": 7,
"id": "eb07a439",
"metadata": {
"execution": {
"iopub.execute_input": "2026-08-11T20:31:10.853315Z",
"iopub.status.busy": "2026-08-11T20:31:10.853214Z",
"iopub.status.idle": "2026-08-11T20:31:11.243033Z",
"shell.execute_reply": "2026-08-11T20:31:11.242377Z"
}
},
"outputs": [],
"source": [
"from sklearn.linear_model import LinearRegression\n",
"\n",
"\n",
"def train_weekly(symbol, train_data, test_data):\n",
" # Predict the next weekly return from the weekly close.\n",
" returns = train_data[\"close\"].pct_change().shift(-1)\n",
" train_rows = train_data.assign(pred=returns).dropna()\n",
" model = LinearRegression()\n",
" model.fit(train_rows[[\"close\"]], train_rows[[\"pred\"]])\n",
" return model, [\"close\"]\n",
"\n",
"\n",
"model_weekly = pybroker.model(\"weekly_slr\", train_weekly)\n",
"\n",
"\n",
"def hold_with_model(ctx):\n",
" preds = ctx.interval(\"weekly\").preds(\"weekly_slr\")\n",
" if len(preds) == 0:\n",
" return\n",
" if not ctx.long_pos():\n",
" if preds[-1] > 0:\n",
" ctx.buy_shares = 100\n",
" elif preds[-1] < 0:\n",
" ctx.sell_all_shares()\n",
"\n",
"\n",
"strategy = Strategy(yfinance, start_date=\"1/1/2021\", end_date=\"1/1/2026\")\n",
"strategy.add_execution(\n",
" hold_with_model,\n",
" [\"AMD\", \"NVDA\", \"INTC\"],\n",
" models=model_weekly.intervals(\"weekly\"),\n",
")"
]
},
{
"cell_type": "markdown",
"id": "21c7019a-0e82-4399-abed-9e858c0b71ff",
"metadata": {},
"source": [
"During [Walkforward Analysis](https://www.pybroker.com/en/latest/reference/pybroker.strategy.html#pybroker.strategy.Strategy.walkforward), the `lookahead` between an interval model's train and test data is enforced using the compressed bar units in order to prevent future leakage."
]
},
{
"cell_type": "code",
"execution_count": 8,
"id": "e0a14449-6cde-4110-af47-4aa300874890",
"metadata": {
"execution": {
"iopub.execute_input": "2026-08-11T20:31:11.244828Z",
"iopub.status.busy": "2026-08-11T20:31:11.244663Z",
"iopub.status.idle": "2026-08-11T20:31:11.340618Z",
"shell.execute_reply": "2026-08-11T20:31:11.340159Z"
}
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Backtesting: 2021-01-01 00:00:00 to 2026-01-01 00:00:00\n",
"\n"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"Loaded cached bar data.\n",
"\n"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"Train split: 2021-01-07 00:00:00 to 2022-04-04 00:00:00\n"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"Finished training models: 0:00:00 \n",
"\n"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"Test split: 2022-04-05 00:00:00 to 2023-07-05 00:00:00\n"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
"\r",
"\u001b[38;2;255;0;0m 0%\u001b[39m \u001b[38;2;255;0;0m(0 of 313)\u001b[39m | | Elapsed Time: 0:00:00 ETA: --:--:--"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
"\r",
"\u001b[38;2;0;255;0m100%\u001b[39m \u001b[38;2;0;255;0m(313 of 313)\u001b[39m |######################| Elapsed Time: 0:00:00 Time: 0:00:00"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
"\n"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"\n"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"Train split: 2022-04-05 00:00:00 to 2023-07-05 00:00:00\n"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"Finished training models: 0:00:00 \n",
"\n"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"Test split: 2023-07-06 00:00:00 to 2024-10-01 00:00:00\n"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
"\r",
"\u001b[38;2;255;0;0m 0%\u001b[39m \u001b[38;2;255;0;0m(0 of 313)\u001b[39m | | Elapsed Time: 0:00:00 ETA: --:--:--"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
"\r",
"\u001b[38;2;0;255;0m100%\u001b[39m \u001b[38;2;0;255;0m(313 of 313)\u001b[39m |######################| Elapsed Time: 0:00:00 Time: 0:00:00"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
"\n"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"\n"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"Train split: 2023-07-06 00:00:00 to 2024-10-01 00:00:00\n"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"Finished training models: 0:00:00 \n",
"\n"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"Test split: 2024-10-02 00:00:00 to 2025-12-31 00:00:00\n"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
"\r",
"\u001b[38;2;255;0;0m 0%\u001b[39m \u001b[38;2;255;0;0m(0 of 313)\u001b[39m | | Elapsed Time: 0:00:00 ETA: --:--:--"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
"\r",
"\u001b[38;2;0;255;0m100%\u001b[39m \u001b[38;2;0;255;0m(313 of 313)\u001b[39m |######################| Elapsed Time: 0:00:00 Time: 0:00:00"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
"\n"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"\n"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"Finished backtest: 0:00:00\n"
]
},
{
"data": {
"text/html": [
"\n",
"\n",
"
\n",
" \n",
" \n",
" | \n",
" name | \n",
" value | \n",
"
\n",
" \n",
" \n",
" \n",
" | 0 | \n",
" trade_count | \n",
" 10 | \n",
"
\n",
" \n",
" | 1 | \n",
" initial_market_value | \n",
" 100000.0 | \n",
"
\n",
" \n",
" | 2 | \n",
" end_market_value | \n",
" 123627.0 | \n",
"
\n",
" \n",
" | 3 | \n",
" total_pnl | \n",
" 23627.0 | \n",
"
\n",
" \n",
" | 4 | \n",
" unrealized_pnl | \n",
" 0.0 | \n",
"
\n",
" \n",
" | 5 | \n",
" total_return_pct | \n",
" 23.627 | \n",
"
\n",
" \n",
" | 6 | \n",
" total_profit | \n",
" 25186.0 | \n",
"
\n",
" \n",
" | 7 | \n",
" total_loss | \n",
" -1559.0 | \n",
"
\n",
" \n",
" | 8 | \n",
" total_fees | \n",
" 0.0 | \n",
"
\n",
" \n",
" | 9 | \n",
" max_drawdown | \n",
" -13776.0 | \n",
"
\n",
" \n",
" | 10 | \n",
" max_drawdown_pct | \n",
" -12.158334 | \n",
"
\n",
" \n",
" | 11 | \n",
" max_drawdown_date | \n",
" 2025-04-08 00:00:00 | \n",
"
\n",
" \n",
" | 12 | \n",
" win_rate | \n",
" 90.0 | \n",
"
\n",
" \n",
" | 13 | \n",
" loss_rate | \n",
" 10.0 | \n",
"
\n",
" \n",
" | 14 | \n",
" winning_trades | \n",
" 9 | \n",
"
\n",
" \n",
" | 15 | \n",
" losing_trades | \n",
" 1 | \n",
"
\n",
" \n",
" | 16 | \n",
" avg_pnl | \n",
" 2362.7 | \n",
"
\n",
" \n",
" | 17 | \n",
" avg_return_pct | \n",
" 44.464 | \n",
"
\n",
" \n",
" | 18 | \n",
" avg_trade_bars | \n",
" 192.6 | \n",
"
\n",
" \n",
" | 19 | \n",
" avg_profit | \n",
" 2798.444444 | \n",
"
\n",
" \n",
"
\n",
"
"
],
"text/plain": [
" name value\n",
"0 trade_count 10\n",
"1 initial_market_value 100000.0\n",
"2 end_market_value 123627.0\n",
"3 total_pnl 23627.0\n",
"4 unrealized_pnl 0.0\n",
"5 total_return_pct 23.627\n",
"6 total_profit 25186.0\n",
"7 total_loss -1559.0\n",
"8 total_fees 0.0\n",
"9 max_drawdown -13776.0\n",
"10 max_drawdown_pct -12.158334\n",
"11 max_drawdown_date 2025-04-08 00:00:00\n",
"12 win_rate 90.0\n",
"13 loss_rate 10.0\n",
"14 winning_trades 9\n",
"15 losing_trades 1\n",
"16 avg_pnl 2362.7\n",
"17 avg_return_pct 44.464\n",
"18 avg_trade_bars 192.6\n",
"19 avg_profit 2798.444444"
]
},
"execution_count": 8,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"result = strategy.walkforward(windows=3, train_size=0.5, timeframe=\"1d\")\n",
"result.metrics_df.head(20)"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3 (ipykernel)",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.12.13"
}
},
"nbformat": 4,
"nbformat_minor": 5
}